<!DOCTYPE html>
<html lang="en">
<title>opentype.js – JavaScript parser/writer for OpenType and TrueType fonts.</title>
<meta name="description" content="A JavaScript library to manipulate the letterforms of text from the browser or node.js.">
<meta charset="utf-8">
<link rel="stylesheet" href="site.css">
<script src="site.js"></script>

<div class="header">
    <div class="container">
        <h1><a href="./">opentype.js</a></h1>
        <nav>
            <a href="font-inspector.html">Font Inspector</a>
            <a href="glyph-inspector.html">Glyph Inspector</a>
        </nav>
        <nav class="right">
            <a class="github" href="https://github.com/opentypejs/opentype.js">Fork me on GitHub</a>
        </nav>
    </div>
</div>

<form class="container" name="demo">

    <div class="explain">
        opentype.js is an OpenType and TrueType font parser and writer.
        It allows you to access the <strong>letterforms</strong> of text from the browser or node.js.
    </div>

    <input id="file" type="file">
    <output class="info" name="fontname"></output>
    <canvas id="preview" width="940" height="300" class="text"></canvas>
    <output id="message"></output>
    <input class="text-input" value="Hello, World!" autofocus name="textField">
    <label>Font Size<input type="range" min="6" max="500" step="2" value="150" name="fontsize" autocomplete="off" oninput="nextElementSibling.innerText=value"><output>150</output></label>
    <label><input type="checkbox" name="drawPoints" checked="checked">Draw Points</label>
    <label><input type="checkbox" name="drawMetrics">Draw Metrics</label>
    <label><input type="checkbox" name="kerning" checked="checked">Kerning</label>
    <label><input type="checkbox" name="ligatures" checked="checked">Ligatures</label>
    <label class="disabled"><input name="hinting" type="checkbox" disabled="true">Hinting</label>

    <div id="variation-options"></div>

    <hr>

    <div class="explain">
        Once you have the shapes, you can modify them, for example by <strong>snapping</strong> them to a virtual grid:
    </div>

    <canvas id="snap" width="940" height="300" class="text"></canvas>
    <label>Strength <input type="range" min="0" max="100" value="80" name="snapStrength" oninput="nextElementSibling.innerText=value"/><output>80</output></label>
    <label>Distance<input type="range" min="1" max="100" value="53" name="snapDistance" oninput="nextElementSibling.innerText=value"/><output>53</output></label>
    <label>X<input type="range" min="-100" max="100" value="0" name="snapX" oninput="nextElementSibling.innerText=value"/><output>0</output></label>
    <label>Y<input type="range" min="-100" max="100" value="0" name="snapY" oninput="nextElementSibling.innerText=value"/><output>0</output></label>

    <hr>

    <div class="explain">
        <h1>Glyphs</h1>
        opentype.js provides you with <strong>raw access</strong> to the glyphs so you can modify them as you please.
    </div>

    <div id="glyphs">
    </div>
    <div class="message">Only the first 100 glyphs are shown.</div>

    <hr>

    <div class="explain">
        <h1>Free Software</h1>
        <p>opentype.js is available on <a href="https://github.com/opentypejs/opentype.js">GitHub</a> under the <a href="https://raw.github.com/opentypejs/opentype.js/master/LICENSE">MIT License</a>.</p>
        <p>Copyright &copy; 2020 Frederik De Bleser.</p>
    </div>

    <hr>
</form>


<script type="module">
import * as opentype from "/dist/opentype.mjs";

const form = document.forms.demo;
form.oninput = renderText;
let font = null;

let options = {};

function doSnap(path) {
    const layers = path._layers;
    if ( layers && layers.length ) {
        for( let l = 0; l < layers.length; l++ ) {
            doSnap(layers[l]);
        }
        return;
    }
    // Round a value to the nearest "step".
    const snap = (v, distance, strength) => (v * (1.0 - strength)) + (strength * Math.round(v / distance) * distance);
    const strength = +form.snapStrength.value / 100.0;
    const snapDistance = +form.snapDistance.value;
    const snapX = +form.snapX.value;
    const snapY = +form.snapY.value;
    for (let i = 0; i < path.commands.length; i++) {
        var cmd = path.commands[i];
        if (cmd.type !== 'Z') {
            cmd.x = snap(cmd.x + snapX, snapDistance, strength) - snapX;
            cmd.y = snap(cmd.y + snapY, snapDistance, strength) - snapY;
        }
        if (cmd.type === 'Q' || cmd.type === 'C') {
            cmd.x1 = snap(cmd.x1 + snapX, snapDistance, strength) - snapX;
            cmd.y1 = snap(cmd.y1 + snapY, snapDistance, strength) - snapY;
        }
        if (cmd.type === 'C') {
            cmd.x2 = snap(cmd.x2 + snapX, snapDistance, strength) - snapX;
            cmd.y2 = snap(cmd.y2 + snapY, snapDistance, strength) - snapY;
        }
    }
}

function renderText() {
    const font = window.font;
    if (!font) return;
    const textToRender = conditionalSampleText(font, form.textField.value);

    var previewCtx = document.getElementById('preview').getContext('2d');
    var options = Object.assign({}, font.defaultRenderOptions, getDrawOptions());
    previewCtx.clearRect(0, 0, 940, 300);
    const fontSize = +form.fontsize.value;
    font.draw(previewCtx, textToRender, 0, 200, fontSize, options);
    if (form.drawPoints.checked) {
        font.drawPoints(previewCtx, textToRender, 0, 200, fontSize, options);
    }
    if (form.drawMetrics.checked) {
        font.drawMetrics(previewCtx, textToRender, 0, 200, fontSize, options);
    }

    const snapPath = font.getPath(textToRender, 0, 200, fontSize, options);
    doSnap(snapPath);
    var snapCtx = document.getElementById('snap').getContext('2d');
    snapCtx.clearRect(0, 0, 940, 300);
    snapPath.draw(snapCtx);
}

function getDrawOptions() {
    const options = {
        kerning: form.kerning.checked,
        hinting: form.hinting.checked,
        features: {
            liga: form.ligatures.checked,
            rlig: form.ligatures.checked
        }
    };
    window.fontOptions = Object.assign({}, window.font.defaultRenderOptions, window.fontOptions, options);
    return window.fontOptions;
}

window.redraw = function(options = { withColors: true, withVariations: true }) {
    if ( options.withVariations ) {
        updateVariationOptions();
    }
};

window.throttledRedraw = throttleDebounce((...args) => {
    window.redraw(...args);
}, 50);

function enableHighDPICanvas(canvas) {
    if (typeof canvas === 'string') {
        canvas = document.getElementById(canvas);
    }
    var pixelRatio = window.devicePixelRatio || 1;
    if (pixelRatio === 1) return;
    var oldWidth = canvas.width;
    var oldHeight = canvas.height;
    canvas.width = oldWidth * pixelRatio;
    canvas.height = oldHeight * pixelRatio;
    canvas.style.width = oldWidth + 'px';
    canvas.style.height = oldHeight + 'px';
    canvas.getContext('2d').scale(pixelRatio, pixelRatio);
}

// Create a canvas and adds it to the document.
// Returns the 2d drawing context.
function createGlyphCanvas(glyph, size) {
    var canvasId, html, glyphsDiv, wrap, canvas, ctx, pixelRatio;
    canvasId = 'c' + glyph.index;
    html = '<div class="wrapper" style="width:' + size + 'px"><canvas id="' + canvasId + '" width="' + size + '" height="' + size + '"></canvas><span>' + glyph.index + '</span></div>';
    glyphsDiv = document.getElementById('glyphs');
    wrap = document.createElement('div');
    wrap.innerHTML = html;
    glyphsDiv.appendChild(wrap);
    canvas = document.getElementById(canvasId);
    ctx = canvas.getContext('2d');
    enableHighDPICanvas(canvas, ctx);
    return ctx;
}

function showErrorMessage(message) {
    var el = document.getElementById('message');
    if (!message || message.trim().length === 0) {
        el.style.display = 'none';
    } else {
        el.style.display = 'block';
    }
    el.innerHTML = message;
}

function onFontLoaded(font) {
    if (window.font) {
        window.font.onGlyphUpdated = null
    }
    window.font = font;
    options = Object.assign({}, window.font.defaultRenderOptions);
    window.fontOptions = options;
    
    updateVariationOptions();

    // Show the first 100 glyphs.
    const glyphsDiv = document.getElementById('glyphs');
    glyphsDiv.innerHTML = '';

    const amount = Math.min(100, font.glyphs.length);
    const x = 50;
    const y = 120;
    const fontSize = 72;
    const ctxs = new Array(amount);
    for (let i = 0; i < amount; i++) {
        const glyph = font.glyphs.get(i);
        const ctx = createGlyphCanvas(glyph, 150);
        ctxs[i] = ctx;
        glyph.draw(ctx, x, y, fontSize, {}, font);
        glyph.drawPoints(ctx, x, y, fontSize);
        glyph.drawMetrics(ctx, x, y, fontSize);
    }

    const hintingCheckbox = form.hinting;
    const hintingLabel = form.hinting.parentElement;
    if (font.hinting) {
        hintingCheckbox.disabled = false;
        hintingLabel.className = '';
    } else {
        hintingCheckbox.disabled = true;
        hintingCheckbox.checked = false;
        hintingLabel.className = 'disabled';
    }

    renderText();

    font.onGlyphUpdated = (glyphId) => {
        if (0 <= glyphId && glyphId < amount) {
            const glyph = font.glyphs.get(glyphId);
            const ctx = ctxs[glyphId];
            glyph.draw(ctx, x, y, fontSize, {}, font);
            glyph.drawPoints(ctx, x, y, fontSize);
            glyph.drawMetrics(ctx, x, y, fontSize);
        }

        const textToRender = form.textField.value;
        const glyphIds = font.stringToGlyphIndexes(textToRender, getDrawOptions());
        if (glyphIds.includes(glyphId)) {
            renderText();
        }
    };
}
const loadScript = (src) => new Promise((onload) => document.head.append(Object.assign(document.createElement('script'), {src, onload})));
async function display(file, name) {
    form.fontname.innerText = name;
    const isWoff2 = name.endsWith('.woff2');
    if (isWoff2 && !window.Module) {
        const wasmReady = new Promise((onRuntimeInitialized) => window.Module = { onRuntimeInitialized });
        await loadScript('https://unpkg.com/wawoff2@2.0.1/build/decompress_binding.js').then(() => wasmReady);
    }
    try {
        const data = await file.arrayBuffer();
        let error;
        try {
            const base64String = arrayBufferToBase64(data);
            sessionStorage.setItem('fontData', base64String);
        } catch (err) {
            showErrorMessage('Error loading font from localStorage');
            error = err;
        }     

        onFontLoaded(opentype.parse(isWoff2 ? Module.decompress(data) : data, { lowMemory: true }));
        if ( !error ) showErrorMessage('');
    } catch (err) {
        showErrorMessage(err.toString());
    }
}

form.file.onchange = function(e) {
    display(e.target.files[0], e.target.files[0].name);
};

enableHighDPICanvas('preview');
enableHighDPICanvas('snap');

const fontData = sessionStorage.getItem('fontData');
if ( fontData ) {
    const arrayBuffer = base64ToArrayBuffer(fontData);
    onFontLoaded(opentype.parse(arrayBuffer));
} else {
    const fontFileName = 'fonts/FiraSansMedium.woff';
    display(await fetch(fontFileName), fontFileName);
}
</script>
